In [683]:
import pandas as pd
import os
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
import re
from IPython.display import clear_output
import matplotlib.pyplot as plt
from sklearn.metrics import det_curve, DetCurveDisplay
pd.set_option('display.max_colwidth', None)
from sklearn.decomposition import PCA
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis as LDA
from sklearn.preprocessing import StandardScaler
import plotly.graph_objects as go
import pickle
import plotly

Zastosowanie post-processingu na danych.¶

W raporcie stosuję trzy różne rodzaje post-processingu: centrowanie danych, PCA i LDA. Tego rodzaju post-processing stosuje się, aby wykorzystać również wartości ujemne. Wektory, które są reprezentacją naszych nagrań, zarówno enrollment, jak i ewaluacyjnych, mają wartości wyłącznie w zakresie od 0 do 1. Dzieje się tak, ponieważ wartości te są prawdopodobieństwami przynależności próbek do komponentów w modelu GMM. Dzięki zastosowaniu post-processingu możemy wydobyć więcej informacji z naszych danych.

Wczytywanie embeddingów.¶

In [684]:
#ścieżka do folderu gdzie przechowywane są dane z embeddingiem
embedded_data_folder = "C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data"

#pobranie wszystkich nazw folderów w podanym katalogu - czyli do folderów każdej osoby
subfolders = [f for f in os.listdir(embedded_data_folder) if os.path.isdir(os.path.join(embedded_data_folder, f))]

#tworzę ścieżki do embeddingów każdej z osób
paths_with_ID = [embedded_data_folder + '/' + subfolder for subfolder in subfolders]

#sortuję ścieżki według ID danej osoby
paths_with_ID = sorted(paths_with_ID, key=lambda x: int(x.split('embedded_data')[-1]))

Powyższy kod wczytuje ścieżki do embeddingów 25 kobiet i 25 mężczyzn. Dane te zostały przygotowane w poprzednich raportach.

In [685]:
paths_with_ID[0:5]
Out[685]:
['C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data0',
 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data1',
 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data2',
 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data3',
 'C:/Users/zbugo/Desktop/praktyki_zadania/13/embedded_data/embedded_data4']

Możemy zobaczyć, jak wyglądają ścieżki do embeddingów.

In [686]:
#lista na ramki z embedingami każdej z osób 
list_for_eval_data_frames = []
#ramka przechowująca wszytkie emgadding enrollment
data_frame_enroll_data = pd.DataFrame()
#iterator tworzę by śledzić postęp działania pęti
iterator = 0

for path_with_ID in paths_with_ID:
    
    #tworzę ścieżki do embedding enrollment każdej z osób 
    path_to_enroll = path_with_ID + '/enroll/embedded_enrol' + str(iterator) + '.csv'
    #wczytuję embedding enrollment i iteracyjnie dodaje go do ramki - wiersze to enrollmenty osób
    data_frame_enroll_data = pd.concat([data_frame_enroll_data, pd.read_csv(path_to_enroll)], ignore_index=True)

    #wyciągam ścieżki do nagrań testowych danej osoby
    path_to_eval = path_with_ID + '/eval/'
    all_paths_to_eval = [f for f in os.listdir(path_to_eval) if os.path.isfile(os.path.join(path_to_eval, f))]
    all_paths_to_eval = [path_to_eval + path for path in all_paths_to_eval]
    #sortuję ścieżki, aby były uporządkowane według ID nagrania testowego
    all_paths_to_eval = sorted(all_paths_to_eval, key=lambda x: int(re.search(r'(\d+)(?=\.csv)', x).group()))

    #tworzę ramkę danych w której wiersze będą embeddingi eval JEDNEJ OSOBY - wiersze to różne nagrania tej samej osoby
    df_eval = pd.DataFrame()

    #iteracyjnie dorzucam nagrania
    for path in all_paths_to_eval:
        df_eval = pd.concat([df_eval, pd.read_csv(path)], ignore_index=True)

    #do listy wrzucam ramki, gdzie każda ramka odpodiada jednej osobie
    list_for_eval_data_frames.append(df_eval)
    
    #śledzę postęp działania pętli
    clear_output(wait=True)
    iterator = iterator + 1
    print(iterator/len(paths_with_ID))
1.0

W powyższym bloku kodu wczytuję embeddingi. Dla enrollment istnieje jedna ramka danych, w której każdy wiersz odpowiada innej osobie. Ewaluacyjne nagrania są przechowywane w liście — każdy element listy to ramka danych odpowiadająca jednej osobie. W tej ramce (jednej z listy), wiersze reprezentują nagrania (oczywiście wszystkie nagrania/wiersze należą do jednej osoby).

In [687]:
data_frame_enroll_data.head()
Out[687]:
0 1 2 3 4 5 6 7 8 9 ... 54 55 56 57 58 59 60 61 62 63
0 0.016061 0.037225 0.033713 0.010862 0.000000 0.017374 3.051719e-02 0.002731 8.254007e-11 0.009259 ... 4.627383e-02 0.026662 0.025655 0.001715 0.000896 0.000698 0.017517 0.000243 0.056859 0.020914
1 0.004948 0.010180 0.014570 0.042996 0.096595 0.054379 6.245632e-03 0.000186 9.793247e-15 0.003365 ... 3.510889e-04 0.013322 0.014190 0.004361 0.001310 0.001961 0.019742 0.000064 0.085954 0.024428
2 0.010735 0.022861 0.057958 0.018219 0.000000 0.014890 2.277481e-16 0.000432 5.257036e-04 0.016517 ... 8.871839e-03 0.010189 0.019216 0.002227 0.000803 0.000264 0.028128 0.000024 0.033623 0.051148
3 0.014271 0.070275 0.049537 0.003289 0.000000 0.031889 8.669394e-04 0.004757 2.212869e-05 0.024013 ... 1.005580e-07 0.010308 0.001131 0.000296 0.009197 0.000155 0.001437 0.000191 0.019623 0.012423
4 0.006863 0.018319 0.028656 0.014880 0.023183 0.007494 2.514921e-03 0.002040 1.211667e-29 0.002670 ... 6.984625e-04 0.045256 0.046806 0.003278 0.001018 0.000615 0.017684 0.000967 0.015770 0.008685

5 rows × 64 columns

In [688]:
list_for_eval_data_frames[0].head()
Out[688]:
0 1 2 3 4 5 6 7 8 9 ... 54 55 56 57 58 59 60 61 62 63
0 0.072280 0.048442 0.062727 0.004963 0.0 0.020369 4.285654e-97 0.000307 4.006581e-14 0.000481 ... 0.001935 0.011524 0.017425 0.001197 0.000339 0.000534 0.009788 0.000208 0.076893 0.053448
1 0.066883 0.028970 0.052333 0.002179 0.0 0.024819 0.000000e+00 0.000579 2.239713e-28 0.000163 ... 0.000769 0.008180 0.009396 0.001423 0.000335 0.000395 0.015831 0.000272 0.064757 0.037688
2 0.034561 0.051490 0.040978 0.004110 0.0 0.018776 0.000000e+00 0.001204 7.604111e-20 0.000099 ... 0.008862 0.028086 0.023853 0.000278 0.000380 0.000697 0.004044 0.000598 0.053900 0.028599
3 0.039741 0.031169 0.057277 0.003557 0.0 0.017032 0.000000e+00 0.001483 6.741872e-15 0.001204 ... 0.000329 0.016589 0.018547 0.000514 0.000497 0.005325 0.009947 0.000545 0.052856 0.027626
4 0.033711 0.029811 0.061313 0.001620 0.0 0.033861 0.000000e+00 0.001471 1.502588e-16 0.000011 ... 0.002871 0.023060 0.012113 0.000276 0.001265 0.000121 0.001894 0.000493 0.075609 0.019588

5 rows × 64 columns

Jest 64 kolumny, ponieważ wybrałem tyle komponentów w modelu GMM.

In [689]:
#ramka gdzie każdy wiersz jest embeddingiem innego nagrania z zestawu uczącego model UBM
with open("mean_predict_proba_df.pkl", 'rb') as file:
    mean_predict_proba_df = pickle.load(file)

Wczytuję embeddingi nagrań, na których trenowałem model UBM.

Funkcja do cross-checkingu i listy potrzebne na przestrzeni całego raportu.¶

In [690]:
def cross_checking(enroll_data, list_eval_data):
    #lista na score dla wszystkich osób
    score_list_for_all_person = []
    #liasta na id osoby do której należy nagranie enrollment 
    id_enroll = []
    #analogicznie, z tym że do kogo należy nagranie ewaluacyjnie 
    id_eval = []
    #lista typu bool, informująca czy była to próba logowania przez autorywanego użytkownika czy próba włamania
    is_genuine = []

    #w pętli liczę podobieństwo kosinusowne które będzie oceniać podobieńtwo enrollmentu i nagrania ewaluacyjnego
    for i in range(0, len(enroll_data)):
        #pobieram pierwszy embedding enrollment
        emdedding_enrollment = pd.DataFrame(enroll_data.loc[i]).T
        
        #lista w której będzie sprawdzane podobieńtwo jednego enrollmentu ze wszystkimi nagraniami ewaluacyjnymi 
        score_list_for_one_person = []

        #w pętli liczę podobieńśtwo kosinusowe enrollmentu z nagraniami ewaluacyjnymi
        for j in range(0, len(list_eval_data)):
            #biorę jedno nagranie ewaluacyjne 
            df_eval = list_eval_data[j]

            #liczę podobieństwo kosinusowe pomiedzy nagraniem enrollment i ewaluacyjnym
            cos_sim = cosine_similarity(emdedding_enrollment, df_eval)[0]
            #wrzucam je do listy 
            score_list_for_one_person.extend(cos_sim)
            #zapisuję czyje było nagranie enrollmentowe
            id_enroll.extend([i] * len(cos_sim))
            #zapisuję czyje było nagranie ewaluacyjne
            id_eval.extend([j] * len(cos_sim))
            #zapisuję czy była to próba włamania czy logowania użytkownika autoryzowanego
            is_genuine.extend([i==j] * len(cos_sim))


        #listę w której sprawdzone są podobieństwa JEDNEGO nagrania enrollment do wszystkich ewaluacyjnych wrzucam do listy
        #na koniec będzie zawierać tyle list ile jest nagrań enrollment, a każdy element listy będzie to lista podbieństw jednego enrollmentu do ewaluacyjnych
        score_list_for_all_person.extend(score_list_for_one_person)
            

    #wszyskie wcześniej zapisane rzeczy zapisuję w jednej ramce
    df_scores = pd.DataFrame({
        'id_enroll' : id_enroll,
        'id_eval' : id_eval,
        'is_genuine' : is_genuine,
        'score' : score_list_for_all_person
    })

    #funkcja zwraca wyżej stworzoną ramkę 
    return df_scores

Powyższa funkcja służy do cross-checkingu. Przyjmuje ramkę danych enroll_data – embeddingi enrollment, gdzie każdy wiersz odpowiada innej osobie, oraz list_eval_data – listę, w której każdy element to ramka danych odpowiadająca innej osobie. W tych ramkach wiersze reprezentują nagrania ewaluacyjne należące do jednej osoby.

In [691]:
df_curve_list = []
colors = []
postprocessing_name = []
all_data = []
list_for_FRR_FAR = []

Listy, które zostaną wykorzystane do każdego rodzaju post-processingu.

Dane bez zastosowania post-processingu.¶

In [692]:
#wczytuję ramkę z cross-cheskingiem stworzonym w poprzenich raportach
without_postprocessing = pd.read_csv('C:/Users/zbugo/Desktop/praktyki_zadania/14/long_data_frame_2.csv')

#dzielę powyższą ramkę na dwie - jedna odpowiada próbom logowania się przez osobę autoryzowaną a druga próbom włamania 
genuine = without_postprocessing[without_postprocessing['genuine']]
impostor = without_postprocessing[~without_postprocessing['genuine']]

Wczytuję dane po cross-checkingu, co zostało zrobione w poprzednich raportach. Następnie dzielę je na dwie ramki: te, gdzie była próba włamania, i te, gdzie logował się użytkownik autoryzowany.

In [693]:
genuine.head()
Out[693]:
ID_enrollment enrollment_file ID_test test_file genuine score
0 0 embedded_enrol0.csv 0 embedded_eval0.csv True 0.842341
1 0 embedded_enrol0.csv 0 embedded_eval1.csv True 0.787785
2 0 embedded_enrol0.csv 0 embedded_eval6.csv True 0.797577
3 0 embedded_enrol0.csv 0 embedded_eval8.csv True 0.841496
4 0 embedded_enrol0.csv 0 embedded_eval10.csv True 0.818735

Ramka impostor wygląda analogicznie, z tym że wszystkie wartości dla genuine będą oznaczone jako false, a wynik (score) będzie mniejszy.

In [694]:
data = genuine, impostor
all_data.append(data)

Umieszczam je w liście, ponieważ będą potrzebne podczas tworzenia wykresów.

In [695]:
#liczę FAR i FRR
FAR, FRR, thresholds = det_curve(y_true=without_postprocessing['genuine'], y_score=without_postprocessing['score'])
#szukam punktu EER, czyli tam gdzie krzywe FAR i FRR się przecinają 
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))

#zapisuję wartości FAR i FRR wraz z thresholdem w taki spsób, aby łatwo później było stworzyć wykresy
df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

#zapisuję wszytskie rzeczy które policzyłem i nadaję pewne cechy post-processingowi aby był rozróżnialny na wykresie
df_curve_list.append(df_curve)
colors.append('green')
postprocessing_name.append('no postproces')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

W powyższym kodzie obliczane są FAR i FRR, a także zapisywane są charakterystyczne cechy danego post-processingu, które będą wykorzystane przy tworzeniu wykresów (np. kolor, nazwa post-processingu itp.). Ogólnie przygotowuję dane do narysowania wykresów.

Centrowanie danych.¶

Od tego miejsca nie będę już tak szczegółowo opisywać działania kodu, ponieważ będzie on identyczny lub analogiczny do tego opisanego wyżej.¶

In [696]:
#ramka danych na zcentrowane dane enrollmentowe
pd_centered_enroll = pd.DataFrame()

#liczę średnie ze zbioru na którym nauczyłem model UBM, średnie dla każdego komponetu 
to_subtract = pd.DataFrame(mean_predict_proba_df.mean()).T.loc[0].values

#w pętli odejmuje wyżej policzone średnie od danych embedded enrollment 
for i in range(0, len(data_frame_enroll_data)):
    row = pd.DataFrame(data_frame_enroll_data.loc[i].values - to_subtract).T
    pd_centered_enroll = pd.concat([pd_centered_enroll, row], ignore_index=True)

#analogiczna lista do tej która przetrzymuje dane ewaluacyjne, z tym że ta będzie przetrzymywać już zcentrowane dane
list_centered_eval_data = []

#w petli centruje nagrania ewaluacyjne
#pętla zewnętrzna bierze ramki w których są wszystkie nagrania ewaluacyjne danej osoby
#pętla wenętrzna bierze każdy wiersz z ramki, czyli każde nagranie ewaluacyjne danej osoby
for df in list_for_eval_data_frames:
    
    df_for_one_person = pd.DataFrame()

    for i in range(0, len(df)):
        one_row = df.loc[i].values
        one_row = pd.DataFrame(one_row - to_subtract).T
        df_for_one_person = pd.concat([df_for_one_person, one_row], ignore_index=True)
        
    list_centered_eval_data.append(df_for_one_person)

Powyższy fragment kodu centruje dane, zarówno embeddingi enrollment, jak i embeddingi ewaluacyjne.

In [697]:
df_scores = cross_checking(enroll_data=pd_centered_enroll, list_eval_data=list_centered_eval_data)

Przeprowadzam cross-checking scentrowanych danych.

In [698]:
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
In [699]:
data = genuine, impostor
all_data.append(data)
In [700]:
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('orange')
postprocessing_name.append('centered data')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

PCA.¶

In [701]:
#Tworzę skaler, aby poprawnie policzyć PCA
scaler = StandardScaler()
scaler.fit(mean_predict_proba_df.values)

#skaluję embedding danych na których nauczony został model
scaled_mean_predict_proba_df = pd.DataFrame(scaler.transform(mean_predict_proba_df.values))

#skaluję embedding enrollment
scaled_enroll = pd.DataFrame(scaler.transform(data_frame_enroll_data.values))

#skaluje również dane ewaluacyjne
scaled_eval_list = []
for eval_df in list_for_eval_data_frames:
    scaled_one_person_eval = scaler.transform(eval_df.values)
    scaled_eval_list.append(pd.DataFrame(scaled_one_person_eval))

Powyżej obliczam skaler i skaluję embeddingi danych, na których został nauczony model UBM, embeddingi enrollment oraz embeddingi ewaluacyjne.

In [702]:
pca = PCA(n_components=0.99, svd_solver='full')
pca.fit(scaled_mean_predict_proba_df.values)


pca_enroll = pca.transform(scaled_enroll.values)
pca_enroll = pd.DataFrame(pca_enroll)


pca_eval_list = []
for scaled_eval_df in scaled_eval_list:
    pca_eval_df = pca.transform(scaled_eval_df.values)
    pca_eval_list.append(pd.DataFrame(pca_eval_df))

Ustawiłem n_components=0.99 i svd_solver='full', ponieważ uważam, że 99% wyjaśnionej wariancji to wystarczający poziom, a dodatkowo może to usunąć znaczną część niepotrzebnych głównych składowych. Ostatecznie nie zależy mi na redukcji wymiarowości, ponieważ nie jest to celem. Celem natomiast jest jak najlepsza dyskryminacja mówców. Robię to z ciekawości, aby sprawdzić, ile głównych składowych zostanie usuniętych.

In [703]:
pca_enroll.shape
Out[703]:
(50, 60)

Widzimy, że PCA zredukowało nasze dane tylko o 4 wymiary, tracąc jedynie 1% wyjaśnionej wariancji. Jednak gdybyśmy byli w stanie zaakceptować nieco większą utratę wyjaśnionej wariancji, moglibyśmy znacznie bardziej zredukować wymiarowość naszych danych.

In [704]:
df_scores = cross_checking(enroll_data=pca_enroll, list_eval_data=pca_eval_list)
In [705]:
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
In [706]:
data = genuine, impostor
all_data.append(data)
In [707]:
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('purple')
postprocessing_name.append('pca')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

LDA.¶

In [708]:
#liczę LDA wykorzsytując dane które użyłem do nauki modelu UBM, z tym że są one po ekstracji (embedding)
lda = LDA()
lda.fit(X=scaled_mean_predict_proba_df.values, y=mean_predict_proba_df.index)

lda_enroll = lda.transform(scaled_enroll.values)
lda_enroll = pd.DataFrame(lda_enroll)


lda_eval_list = []
for scaled_eval_df in scaled_eval_list:
    lda_eval_df = lda.transform(scaled_eval_df.values)
    lda_eval_list.append(pd.DataFrame(lda_eval_df))

Tym razem nie zmieniam żadnych parametrów dla LDA, ponieważ nie zależy mi na redukcji wymiarowości ani na wydajności, gdyż LDA i tak liczy się bardzo szybko. Dlatego skorzystam z domyślnych parametrów.

In [709]:
df_scores = cross_checking(enroll_data=lda_enroll, list_eval_data=lda_eval_list)
In [710]:
genuine = df_scores[df_scores['is_genuine']]
impostor = df_scores[~df_scores['is_genuine']]
In [711]:
data = genuine, impostor
all_data.append(data)
In [712]:
FAR, FRR, thresholds = det_curve(y_true=df_scores['is_genuine'], y_score=df_scores['score'])
arg_of_best_threshold = np.argmin(np.abs(FAR - FRR))
y = np.mean(a = np.array(FAR[arg_of_best_threshold], FRR[arg_of_best_threshold]))
x = thresholds[arg_of_best_threshold]

df_curve = pd.DataFrame({
    'False Acceptance Rate': FAR[::10],
    'False Rejection Rate': FRR[::10]
}, index=thresholds[::10])
df_curve.index.name = "Thresholds"
df_curve.columns.name = "Rate"

df_curve_list.append(df_curve)
colors.append('red')
postprocessing_name.append('lda')
FAR_FRR = FAR, FRR
list_for_FRR_FAR.append(FAR_FRR)

Wykresy.¶

In [713]:
fig, axes = plt.subplots(2, 2, figsize=(18, 8)) 

for i in range(0, len(all_data)):
    row = i // 2  # Wiersz kratki (0 lub 1)
    col = i % 2   # Kolumna kratki (0 lub 1)
    
    # Wybieramy odpowiedni subplot
    ax = axes[row, col]

    ax.hist(all_data[i][0]['score'], bins = 128, alpha = 0.5, label='geniune')
    ax.hist(all_data[i][1]['score'], bins = 128, alpha = 0.5, label='impostor')
    ax.set_xlim(-1, 1)
    ax.legend()
    ax.set_title(postprocessing_name[i])
    ax.grid()
No description has been provided for this image

Widzimy od razu, że wartości score są różne dla różnych rodzajów post-processingu. Pierwszy wykres, zatytułowany 'no postprocess', przedstawia rozkład wartości score bez żadnego post-processingu, i widzimy, że wartości score mieszczą się w zakresie [0, 1]. Zcentrowane dane nie ograniczają się już do tego zakresu, ponieważ mogą być również mniejsze od 0. To samo dotyczy histogramów dla PCA i LDA. Chociaż zmiany w danych są widoczne już na histogramach, bardziej interesujące będą wartości EER, ponieważ to one ostatecznie wskazują, jak dobry jest model.

In [714]:
plotly.offline.init_notebook_mode()
# Lista kolorów dla różnych modeli

# Tworzenie pustego wykresu
fig_thresh = go.Figure()

# Iteracyjne dodawanie krzywych do wykresu
for i, df_curve in enumerate(df_curve_list):
    legend_group = postprocessing_name[i]  # Grupa legendy dla modelu

    # Dodanie linii dla FAR (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=df_curve.index, y=df_curve['False Acceptance Rate'], 
        mode='lines', 
        line=dict(color=colors[i]),
        name='FAR',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla FAR w legendzie
    ))
    
    # Dodanie linii dla FRR (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=df_curve.index, y=df_curve['False Rejection Rate'], 
        mode='lines', 
        line=dict(color=colors[i]),  # Inny styl dla FRR (przerywana linia)
        name='FRR',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla FRR w legendzie
    ))

    # Znalezienie najlepszego threshold (EER)
    arg_of_best_threshold = np.argmin(np.abs(df_curve['False Acceptance Rate'] - df_curve['False Rejection Rate']))
    x = df_curve.index[arg_of_best_threshold]
    y = np.mean([df_curve['False Acceptance Rate'].iloc[arg_of_best_threshold], df_curve['False Rejection Rate'].iloc[arg_of_best_threshold]])

    # Dodanie punktu dla EER (z przypisaną grupą legendy)
    fig_thresh.add_trace(go.Scatter(
        x=[x], y=[y], 
        mode='markers', 
        marker=dict(size=10, color=colors[i]),
        name='EER',
        legendgroup=legend_group,  # Przypisanie do grupy
        showlegend=False  # Ukrycie wpisu dla EER w legendzie
    ))

    # Dodanie tylko jednego wpisu do legendy dla całego modelu
    fig_thresh.add_trace(go.Scatter(
        x=[None], y=[None],  # Wpis do legendy bez dodawania nowych danych
        mode='lines',
        line=dict(color=colors[i]),
        name=postprocessing_name[i],  # Nazwa modelu w legendzie
        legendgroup=legend_group,  # Przypisanie do tej samej grupy
        showlegend=True  # Pokaż w legendzie
    ))

# Dostosowanie osi i tytułu
fig_thresh.update_layout(
    title="FRR, FAR EER for types of post-processing",
    xaxis_title="Thresholds",
    width=1200,
    height=600
)

# Wyświetlenie wykresu
fig_thresh.show()
Wykres jest interaktywny — można przybliżyć widok, naciskając LPM i tworząc okno, które stanie się nowym zakresem widoku. Można również wyłączać wykresy dla nieinteresujących nas post-processingów, klikając odpowiednie nazwy w legendzie, a także śledzić wartości krzywych.¶

Choć na pierwszy rzut oka różnice pomiędzy EER dla różnych post-processingów nie są zbyt widoczne, to po przybliżeniu danych stają się wyraźne. Model najlepiej radzi sobie z danymi po LDA, na drugim miejscu jest PCA, na trzecim centrowanie danych, a jak można było się domyślić, najgorzej wypadają dane bez post-processingu. Wykresy, oprócz umożliwienia odczytania EER, pozwalają śledzić zmianę odsetka fałszywych akceptacji i fałszywych odrzuceń przy konkretnych thresholdach.

In [715]:
plotly.offline.init_notebook_mode()

fig = go.Figure()

for i in range(0,len(list_for_FRR_FAR)):
    FAR, FRR = list_for_FRR_FAR[i]
    fig.add_trace(go.Scatter(x=FAR, y=FRR, name=postprocessing_name[i], mode='lines'))

fig.update_layout(
    title='Krzywa DET',
    xaxis_title='FAR',
    yaxis_title='FRR',
    width=1200,
    height=600
)

fig.update_xaxes(range=[0, 0.35])
fig.update_yaxes(range=[0, 0.55])


fig.show()
Ten wykres również jest interaktywny.¶

Z wykresu możemy odczytać, jak zmienia się FRR (odsetek fałszywych odrzuceń) wraz ze zmianą FAR (odsetka fałszywych akceptacji). Widzimy, że dane po LDA wypadają najlepiej — ich krzywa jest najbliżej lewego dolnego rogu. Łatwo też zauważyć, że model dobrze dyskryminuje również po zastosowaniu PCA w post-processingu, co plasuje PCA na drugim miejscu. Sytuacja komplikuje się w przypadku danych bez post-processingu oraz po centrowaniu. Jednak po dokładnej analizie wykresu, z wykorzystaniem funkcji odczytu wartości na krzywych, można zauważyć, że model lepiej działa z danymi po centrowaniu niż bez żadnych działań. Szukamy na obu krzywych kompromisu pomiędzy FRR i FAR — w przypadku scentrowanych danych możemy znaleźć punkt, dla którego FAR i FRR mają mniejsze wartości niż 0.08. Natomiast w przypadku braku post-processingu nie uda się znaleźć takiego punktu (tym punktem oczywiście jest EER).

In [716]:
fig, ax = plt.subplots(figsize=(18, 8))  # Ustaw szerokość i wysokość w calach

# Tworzenie obiektu DetCurveDisplay i rysowanie wykresu
for i in range(0, len(list_for_FRR_FAR)):

    FAR, FRR = list_for_FRR_FAR[i]
    display = DetCurveDisplay(fpr=FAR, fnr=FRR)
    display.plot(ax=ax, color = colors[i], label = postprocessing_name[i])  # Przekazanie osi 'ax' do metody plot()

ax.set_title('Krzywa DET (Detection Error Tradeoff).')

# Wyświetlenie wykresu
plt.grid();
No description has been provided for this image

Jest to dokładnie ten sam wykres co wyżej, jednak w innej skali i nie jest interaktywny.

Podsumowanie.¶

Najlepszym rodzajem post-processingu okazało się LDA, co nie jest zaskoczeniem, ponieważ jego zadaniem jest tworzenie nowych składowych liniowych, które mają na celu separację klas, w naszym przypadku mówców. LDA można również wykorzystać do redukcji wymiarowości, choć ta cecha jest charakterystyczna także dla PCA. PCA zajęło drugie miejsce — jego głównym zadaniem jest redukcja wymiarowości oraz ortogonalizacja (co również robi LDA). Jednak PCA ma jedną istotną wadę w porównaniu z LDA — nie bierze pod uwagę klas, co wpływa na gorsze działanie. Na trzecim miejscu znajduje się centrowanie danych, prosta metoda polegająca na odjęciu od embeddingów średniego embeddingu z całego zbioru uczącego model UBM. Na ostatnim miejscu uplasował się brak post-processingu.